<?php
namespace DRY\Module;

/**
 * DonReY Framework 2012 :: Object Relational Mapping system - Records and RecordSets
 * This module handles Record objects stored in database, and operating on multiple records at once using RecordSets
 * DonReY does not "write code" to implement an ORM, it instead uses predefined objects with magic functionality (PHP 5),
 *   however, it does allow extending those objects to add special functionality, and the record definition method support a parameter "record class name",
 *   to instance a record object of a special class ( and not the default one )
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/record.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Record
 */
class Record extends base
{
	// Exception/Error codes
	const
		EXC_MALFORMED_RECORD_DEFINITION = 1,
		EXC_RECORD_MODEL_NOT_DEFINED = 2,
		EXC_RECORD_CLASS_NOT_FOUND = 3,
		EXC_OBJECT_NOT_DRY_RECORD = 4,

		EXC_INVALID_RECORD_STATE = 11,
		EXC_INSERT_ID_NOT_RETURNED = 12,
		EXC_RECORD_ALREADY_DESTROYED = 13,
		EXC_FIELDS_NOT_DEFINED = 14;
	// Autoloader configuration
	const
		Autoload_Directory = 'record',
		Autoload_Prefix = 'DRY\Record\Type',
		Autoload_Suffix = 'record'
		;

	/**
	 * All record definitions currently loaded into the Record module.
	 *
	 * @var array
	 */
	protected $definitions;

	/**
	 * Defines a record model (which can be instanced any number of times using the factory_*() methods)
	 * If calling this on an existing model, it redefines that model
	 *
	 * @param string $model How this model of record will be named
	 * @param array $definition Array containing data about how this record behaves
	 * @param string $rec_class Record class name - For future extensibility (to invent other types of more complex records)
	 */
	public function define_Record($model, $definition = null, $rec_class=null)
	{
		// if model not provided, attempt to use the other method
		if(is_null($definition))
			$definition = $this-> define_Record_from_Table($model);
		else {
			if(!is_array($definition))
				throw new \DRY\Water('record', self::EXC_MALFORMED_RECORD_DEFINITION, array('model'=>$model));

			if(!isset($definition['type']))
				$definition['type'] = 'Single'; // Default the type to "Single", since it's the most commonly used
			if(!isset($definition['table']))
				$definition['table'] = $model; // Assume $model as table name if not specified
			if(!isset($definition['id_field']))
				$definition['id_field'] = 'id';
		}

		if(!is_null($rec_class))
			$definition['record_class'] = $rec_class;

		// TODO: Check definition for correctness, and refuse bad definitions
		$this-> definitions[$model] = $definition;
	}

	/**
	 * Creates a definition for a record of type "Single" (or specified) by analysing an existing table (DESCRIBE table)
	 * @param string $table_name Existing table name in the current database
	 * @param string $model How this model of record will be named
	 * @return array The definition ( so it can be stored and reused, since the table's structure doesn't change often - and DESCRIBE is quite slow )
	 */
	public function define_Record_from_Table($table_name, $model=null, $default_type = 'Single')
	{
		if(is_null($model))
			$model = $table_name;

		$definition = array(
			'table'=> $table_name,
			'type'=> $default_type,
			'fields'=> array()
		);
		// attempt to Describe the named table
		$r = $this['connection']-> query("DESCRIBE $table_name");
		foreach($r as $field_desc) {
			// determine the ID field
			if($field_desc['Key']=='PRI' && $field_desc['Extra']=='auto_increment')
				$definition['id_field'] = $field_desc['Field'];
			else {
				// for all else, construct the 'fields' array
				$definition['fields'][$field_desc['Field']] = array(
					'db_type'=> $field_desc['Type'],
					'default'=> $field_desc['Default']
				);
			}
		}

		$this-> definitions[$model] = $definition;
		return $definition;
	}

	/**
	 * TODO: Uses CREATE TABLE to construct an empty table that follows a correct ( and full ) record definition
	 * @param string $model Record model to use
	 * @param string $table_name Table name (optional, if not specified the $model is used)
	 */
	public function create_Table_from_RecordDefinition($model, $table_name = null)
	{

	}

	/**
	 * Creates a record object working (initialy) in "Solo" mode.
	 * @param string $model Record model to use, must exist prior to factorying
	 * @param int $id Default ID to use
	 */
	public function &factory_Record($model, $id=null)
	{
		if(!isset($this-> definitions[$model]))
			throw new \DRY\Water('record', self::EXC_RECORD_MODEL_NOT_DEFINED);

		$record_class = isset($this-> definitions[$model]['record_class'])?$this-> definitions[$model]['record_class']:self::Autoload_Prefix.'_'.$this-> definitions[$model]['type'];
		if(!class_exists($record_class, true))
			throw new \DRY\Water('record', self::EXC_RECORD_CLASS_NOT_FOUND); // problem in the model definition

		$record = new $record_class($this, $model, $id, false);
		if(!$record instanceof \DRY\Record\base)
			throw new \DRY\Water('record', self::EXC_OBJECT_NOT_DRY_RECORD); // bad class type named in model definition

		return $record;
	}

	/**
	 * Creates a record set object, which can be used to create multiple record objects working in "Set" mode.
	 * @param string $model Record model to use, must exist prior to factorying
	 */
	public function factory_RecordSet($model)
	{
		if(!isset($this-> definitions[$model]))
			throw new \DRY\Water(self::EXC_RECORD_MODEL_NOT_DEFINED);

		return new \DRY\Record\Set($this, $model);
	}

	/**
	 * Magic function to return record definition for a given key
	 * @param string $model
	 */
	public function &__get($model)
	{
		return $this-> definitions[$model];
	}

	/**
	 * Checks if the record definition exists
	 *
	 * @param string $model
	 */
	public function __isset($model)
	{
		return isset($this-> definitions[$model]);
	}

}

namespace DRY\Record;

/**
 * Record is a database information abstractization object
 * There are five types of record types planned (but more might be invented later):
 * - "Single" Record - fixed number of fields ( known, in definition ), all data for a record is stored in one table, one record is one row
 * - "Multi" Record - variable number of fields, data is stored in an "attributes" table, with each field on one row
 * - "MultiMap" Record - same as with "Multi", but the definition contains what names of fields could exist, assigns them a number, and in which tables they exist
 * - "Relational" Record - Similar to the "Single" record, but some fields can be records themselves ( of any kind )
 * - "MultiRecord" Record - A "Multi" record which can have other records as fields.
 *
 * Records function in two modes:
 *   Solo - each record does reads/writes individually
 *   Set - the RecordSet dictates when data is read/written, and individual record updates are stored instead, not operated directly
 * A record can only switch mode if attaching/detaching it to/from a RecordSet
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/record.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Record
 */
abstract class base implements \IteratorAggregate
{
	/**
	 * Record Module reference (used to access the models and other functionality)
	 * @var &\DRY\Module\Record
	 */
	protected $_rec;

	/**
	 * Indicates if the record works in Solo mode ( false ) or Set mode ( true )
	 * @var bool
	 */
	protected $_setMode;

	/**
	 * Reference to the model definition (which is actually stored in the \DRY\Module\Record object
	 * @var &array
	 */
	protected $_def;

	/**
	 * Corresponding ID in the table(s) that contains the record.
	 * Is null if the record doesn't exist in the database yet ( is new, or not yet loaded )
	 * @var int|string
	 */
	protected $_id;

	/**
	 * Base Record object constructor
	 * @param \DRY\Module\Record $rec
	 * @param string $model
	 * @param int|string $id
	 * @param bool $set Initial mode of operation
	 */
	public function __construct(\DRY\Module\Record $rec, $model, $id=null, $set = false)
	{
		$this->_rec = &$rec;

		// definition reference
		$this->_def = &$rec-> $model;

		// ID to use ( attempt to use an existing record, or force a new one to be written with this particular ID ( not autogenerated ) )
		if(!is_null($id))
			$this-> id = $id;

		$this->_setMode = $set;
	}

	/**
	 * Switches the mode of operation for this record
	 * DO NOT CALL THIS METHOD DIRECTLY, it's internal to RecordSet's mechanism
	 * @param bool $mode
	 */
	public function _switch_Mode($mode)
	{
		if($this->_setMode == false && $mode == true) {
			// switch from Solo to Set
			$this->_setMode = true;
		} elseif($this->_setMode == true && $mode == false) {
			// switch from Set to Solo
			$this->_setMode = false;
		}
	}

	/**
	 * Converts a raw database field into PHP format (import data), depending on field's definition
	 * @param string $field Field name
	 * @param mixed $raw_field_data
	 */
	protected function convert_Raw($field, $raw_field_data)
	{
		// TODO: for now, return directly - conversions later
		return $raw_field_data;
	}

	/**
	 * Returns the database version of a field's content by exporting PHP format,  depending on field's definition
	 * @param unknown_type $field
	 */
	protected function convert_Internal($field)
	{
		// TODO: for now, just force data to be a string ( since it will be put in a query )
		return (string)$this->_data[$field];
	}

//===== Field access methods =====//

	/**
	 * Memory cache for record fields. Can contain almost any PHP data types (primitive, compound, NULL)
	 * - key = field name
	 * - value = field value / array / object / null
	 * @var array
	 */
	protected $_data=array();

	/**
	 * Record types implement what happens when a field is read
	 * @param string $field
	 */
	abstract public function __get($field);

	/**
	 * Record types implement what happens when a field is written
	 * @param string $field
	 * @param mixed $value Can be anything, depends on record type
	 */
	abstract public function __set($field, $value);

	/**
	 * Checks if a field name is is part of the record's definition
	 * (Some more special records like the "Multi" could actually check if the field is stored in database or not)
	 * @param string $field
	 */
	public function __isset($field)
	{
		return isset($this->_def['fields'][$field]);
	}

	/**
	 * Imports multiple fields at once from a container $data variable (can be object, array, another record or a string that can be transformed into a container (like json, xml, php serialize)
	 * @param mixed $data
	 * @param string $mode What kind of $data is (default assumes one of "Iterator", "array", or DRY_Record_base (any Traversable data type))
	 */
	public function import($data, $mode = 'default')
	{
		switch($mode) {
			case 'json':
				$data = json_decode($data, true);
				break;
			case 'serialize':
				$data = unserialize($data);
				break;
		}
		foreach($data as $key=>$value)
			$this-> $key = $value;
	}

	/**
	 * Exports current record's content into an external object (by default, into a PHP array)
	 * @param bool $add_id True = add the current ID to exported data (if the record is not yet created ( no ID ), it is created
	 * @param string $mode 'array', 'object', 'json'?, 'xml'?...
	 * @return mixed
	 */
	public function export($add_id = false, $mode = 'array')
	{
		// TODO - This only works for single table records, support other types ( by going recursively in Export if some data members are records themselves )

		// attempt to read the record (nothing happens if it's already synched)
		$this-> read();

		if($this-> id == 0)
			$exported_data = null; // if no ID after read operation, then the record doesn't exist, return null
		else {
			// then export the _data
			$exported_data = $this->_data;
			if($add_id)
				$exported_data['id'] = $this-> id;
		}
		switch($mode) {
			case 'array':
				return (array)$exported_data;
			case 'object':
				return (object)$exported_data;
			case 'json':
				return json_encode($exported_data);
			case 'serialize':
				return serialize($exported_data);
		}
	}

//===== Database operations, Solo mode =====//

	/**
	 * (Solo Mode) Reads the complete record from the database (all the fields)
	 * Different record types might read one or more tables and rows to implement this
	 * @param bool $overwrite Forces read and overwrites any changes to the record done before this reading
	 */
	abstract public function read($overwrite=false);

	/**
	 * (Solo Mode) Writes the record into the database, either by creating it when it didn't previously exist, or by updating existing data
	 * Different record types might have complex writing methods ( one table, multiple, one or more rows )
	 * @param bool $force Forces write even if record update flag is set
	 */
	abstract public function write($force=false);


	/**
	 * Removes the record from the database (delete operation)
	 * In (Solo Mode), the record is removed immediatly, in (Set Mode) just the state is set to "destroy", and the record is removed at set write time.
	 */
	abstract public function destroy();

//===== Database operations, Set mode - All methods are underscore _ prefixed, because they shouldn't be called outside RecordSet's implementation =====//

	/**
	 * Indicates if the record's table(s) can be joined with other tables in a read operation ( not all record types support this )
	 * @return bool
	 */
//	abstract public static function _implements_Join(); // PHP won't let me write both "abstract" and "static" at the same time. Duh.

	/**
	 * Assambles the data which will be sent to the multi "Create" or "Update" methods in RecordSet
	 * It is possible that some record types actually DO write in this method, when their storage mode cannot be implemented in a joined write,
	 * and instead it returns just a reference to how to access the written data
	 */
	abstract public function _prepare_WriteContent();

	// for record types that support Join:

	/**
	 * (Optional)
	 * Sets up the query to include the data table(s) used by this record type
	 * @param DRY_QueryFactory_dialect $q
	 * @param array $def Record definition (not available otherwise since this is a static method)
	 * @param string $mode How to attach depends of the type of operation ( 'select', 'update', 'delete' - all support joins )
	 */
//	abstract public static function _attach_Tables(\DRY\Queryfactory\Dialect_base $q, $def, $mode = 'select');

	// for record types that do not support Join

	/**
	 * (Optional)
	 * Reads the data for multiple records, data which RecordSet will use to create several Record Objects
	 * @param \DRY\Module\Record $rec This module's object, to get the connection/qf, since this is a static method
	 * @param array $def Record definition, so this method knows what to read
	 * @param array $id_list
	 */
//	abstract public static function _setRead(\DRY\Module\Record $rec, &$def, $id_list);

	/**
	 * (Optional)
	 * Creates the data for multiple records (data comes from RecordSet, which assambles it by calling _prepare_WriteContent for each stored record)
	 * @param \DRY\Module\Record $rec
	 * @param array $def Record definition, so this method knows what to read
	 * @param array $set_data
	 * @return array ID List (id's of the records just created), so they can be passed to the records inside RecordSet
	 */
//	abstract public static function _setCreate(\DRY\Module\Record $rec, &$def, $set_data);

	/**
	 * (Optional)
	 * Updates the data for multiple records at once (data comes from RecordSet, which assambles it by calling _prepare_WriteContent for each stored record)
	 * @param \DRY\Module\Record $rec
	 * @param array $def Record definition, so this method knows what to read
	 * @param array $set_data
	 * @return array ID List (id's of the records just created), so they can be passed to the records inside RecordSet
	 */
//	abstract public static function _setUpdate(\DRY\Module\Record $rec, &$def, $id_list, $update_data);

	/**
	 * (Optional)
	 * Deletes multiple records by specifying their ID's
	 * @param \DRY\Module\Record $rec
	 * @param array $def Record definition, so this method knows what to read
	 * @param array $id_list
	 */
//	abstract public static function _setDelete(\DRY\Module\Record $rec, &$def, $id_list);

	/**
	 * IteratorAggregate implementation, allows using foreach() on the record, to retrieve all fields one by one
	 */
	public function getIterator()
	{
		return $this->_data;
	}
}

/**
 * A Dry\Record\Set is just a group of records processed together (read together, created/updated/deleted together)
 * It doesn't represent any type of data organization in the database
 */
class Set implements \ArrayAccess, \Countable, \IteratorAggregate
{
	/**
	 * Model of the record used in this Set
	 * @var string
	 */
	protected $model;

	/**
	 * Record Module reference (used to access the models and other functionality)
	 * @var &\DRY\Module\Record
	 */
	protected $_rec;

	/**
	 * Reference to the model definition (which is actually stored in the \DRY\Module\Record object
	 * @var &array
	 */
	protected $_def;

	public function __construct(\DRY\Module\Record $rec, $model)
	{
		$this-> model = $model;

		$this->_rec = &$rec;
		// definition reference
		$this->_def = &$rec-> $model;

		$this-> records = new \SplObjectStorage();
	}

	/**
	 * Destructor initiates a write operation to update all contained records
	 */
	public function __destruct()
	{
		$this-> write();
	}

	/**
	 * Records contained in this RecordSet. Uses SplObjectStorage to hold the records, and provide iterator functions
	 * @var SplObjectStorage
	 */
	protected $records;

	/**
	 * Suplimentary storage (references) for records which have ID's
	 *   key=record ID
	 *   value=Record reference
	 *
	 * @var array[DRY_Record_base]
	 */
	protected $record_index=array();

	/**
	 * Little helper to retrieve the class used by the record model linked to this RecordSet
	 * @return string
	 */
	private function _get_RecordClass()
	{
		return isset($this-> _rec->{$this-> model}['record_class'])?
			$this-> _rec->{$this-> model}['record_class']:
			\DRY\Module\Record::Autoload_Prefix.'_'.$this-> _rec->{$this-> model}['type'];
	}

	/**
	 * Creates a record object working (initialy) in "Set" mode, and attaches it to itself
	 * @param int $id Default ID to use
	 * @return DRY_Record_base
	 */
	public function &factory_Record($id=null)
	{
		// ID is already used, return the associated record
		if(!is_null($id) && isset($this-> records_index[$id]))
			return $this-> records_index[$id];

		$record_class = $this->_get_RecordClass();
		if(!class_exists($record_class))
			throw new \DRY\Water(\DRY\Module\Record::EXC_RECORD_CLASS_NOT_FOUND); // problem in the model definition

		$record = new $record_class($this-> _rec, $this-> model, $id, true);
		if(!$record instanceof base)
			throw new \DRY\Water(\DRY\Module\Record::EXC_OBJECT_NOT_DRY_RECORD); // bad class type named in model definition

		$this-> records-> attach($record, $id); // save reference in record storage
		if(!is_null($id))
			$this-> record_index[$id] = &$record;
		return $record;
	}

	/**
	 * Performs a read operation which will instantiate multiple Record objects
	 *
	 * @param string|callback $query_or_callback
	 * - Query mode, string or DRY_QueryFactory_dialect: Runs an arbitrary SQL query that should theoretically return proper data for the new Records
	 * - Callback mode: Uses a callback to add filters the query which is constructed by this method callback( DRY_QueryFactory_dialect $q)
	 * @return int Returns the number of records that were found
	 */
	public function read($query_or_callback)
	{
		// Determine mode
		if(is_callable($query_or_callback)) {
			$q = $this->_rec['queryfactory']-> select();
			// see what kind of records it deals with:
			$record_class = $this->_get_RecordClass();

			if($record_class::_implements_Join()) {
				// yes, we can join, so compile joins
				$record_class::_attach_Tables($q, $this->_def, 'select');
			}
			// add the callback filter
			$query_or_callback($q);
		} else {
			$q = (string)$query_or_callback;
		}

		// Do the read
		$r = $this->_rec['connection']-> query($q);
		if($r instanceof \DRY\Database\Driver_result_base && $r-> num_rows > 0) {
			// we have some results, create the objects
			foreach($r as $row) {
				$record = $this-> factory_Record($row[$this->_def['id_field']]);
				$record->_load_SetRow($row);
			}
		}
	}

	/**
	 * Mass-Write operation
	 * - Groups all records which need updating into one update (using a temp table and update-join)
	 * - Groups all records which need inseting into one insert with all the data
	 */
	public function write()
	{
		// TODO: Check if record definition contains field information for database, if not, joined update is not possible.
		// In that case, it instructs each record to update individually

		$updates = array();
		$inserts = array();
		$deletes = array();

		// see what kind of records it deals with:
		$record_class = $this->_get_RecordClass();
		if($record_class::_implements_Join()) {
			// yes, we can join, so compile joined updates, combined inserts
			foreach($this-> records as $record) {
				$write_content = $record->_prepare_WriteContent();

				if($write_content === false)
					continue;
				elseif($write_content['mode'] == 'update') { // update mode
					$updates[] = $write_content;
				} elseif($write_content['mode'] == 'insert') { // insert mode
					$write_content['record'] = &$record;
					$inserts[] = $write_content;
					unset($record);
				} else { // delete mode, make list of records that are to be deleted
					if($write_content['id'])
						$deletes[] = $write_content['id'];
				}
			}

			// updates first
			if(!empty($updates)) {
				// TODO: this only works for one table... multi-table (relational) record can't be updated like this
				/* IDEA: $write_content will contain all data from all tables, with table name and field name
				 * Assamble a list of column names for the temp table out of those, and a list of table-to-field, to be passed to the update
				 * Join all tables together, and update fields in the proper table with that info
				 * (Sounds complicated, but not impossible)
				 */

				// use the definition to assamble the temporary table
				$q = $this->_rec['queryfactory']-> create('table')
					-> flags('temporary', 'MEMORY') //array('temporary'=>true, 'engine'=>'MEMORY')
					-> name("RecordSet_Write_updates")
					-> column("id")
					-> index("index", "id");
				foreach($this->_def['fields'] as $field=>$field_def) {
					$q-> column($field, $field_def['db_type'], array('null'=>true));
				}
				$this->_rec['connection']-> query($q);

				// now, insert those values that need updating into the temp table
				$temp_insert_fields = array_keys($this->_def['fields']);
				$temp_insert_fields[] = 'id';
				$q = $this->_rec['queryfactory']-> insert()
					-> table("RecordSet_Write_updates", $temp_insert_fields);
				foreach($updates as $update) {
					$update['data']['id'] = $update['id'];
					$q-> values($update['data']);
				}
				$this->_rec['connection']-> query($q);

				// finally, do the update-join on the record table(s)
				$q = $this->_rec['queryfactory']-> update()
					-> table("RecordSet_Write_updates");
				$record_class::_attach_Tables($q, $this->_def, 'update');

				foreach(array_keys($this->_def['fields']) as $field)
					$q-> values(array($field=>"COALESCE(RecordSet_Write_updates.{$field}, {$this->_def['table']}.{$field})"), $this->_def['table'], true);
				$this->_rec['connection']-> query($q);

				// and drop the temp table, we don't need it anymore
				$this->_rec['connection']-> query("DROP TABLE RecordSet_Write_updates");
			}

			// and now inserts (theoretically, simpler)
			// practically, insert supports only one table at a time, so multi-table records will have to be inserted in each table individually
			if(!empty($inserts)) {
				$q = $this->_rec['queryfactory']-> insert();
				foreach($inserts as &$insert)
					$q-> values($insert['data']);

				// FIXME THIS DOESN'T WORK FOR MULTI TABLE RECORDS!
				$record_class::_attach_Tables($q, $this->_def, 'insert');

				$this->_rec['connection']-> query($q);

				// insert-id is the ID of the first record inserted, so assign ID's to these new records
				$first_insert_id = $this->_rec['connection']-> insert_id;
				if($first_insert_id == 0)
					throw new \DRY\Water('record', \DRY\Module\Record::EXC_INSERT_ID_NOT_RETURNED);

				// finally, set the record's ID's to the values that were just received from the DB Connection
				foreach($inserts as &$insert) {
					$id = $first_insert_id++;
					$insert['record']-> set_ID($id, true);
					$this-> record_index[$id] = &$insert['record'];
				}
			}

			// deletes are simple, just make a list of all the ID's that should be deleted and run the query
			if(!empty($deletes)) {
				$q = $this->_rec['queryfactory']-> delete()
					-> where($this->_def['id_field'].' IN(?)', $deletes);
				$record_class::_attach_Tables($q, $this->_def, 'delete');

				$this->_rec['connection']-> query($q);
			}
		} else {
			// sorry, not possible with grouped updates/inserts/deletes, as the record type doesn't support joins
//			foreach($this-> records as $record)
//				$record-> _individual_Write()?

		}
	}

//===== ArrayAccess Implementation - Allows accessing the RecordSet as an array of Record objects, where key is the ID =====//

	/**
	 * Checks if the record ID is part of the set
	 * @param int $id
	 */
	public function offsetExists($id)
	{
		return isset($this-> record_index[$id]);
	}

	/**
	 * Returns a record object from the set
	 * @param int $id
	 * @return DRY_Record_base
	 */
	public function &offsetGet($id)
	{
		if(isset($this-> record_index[$id]))
			return $this-> record_index[$id];
		else {
			$null = null;
			return $null;
		}
	}

	/**
	 * Attaches an existing record which is in Solo mode to the RecordSet and changes the mode of operation for the record
	 * @param int $id
	 * @param DRY_Record_base $record
	 */
	public function offsetSet($id, $record)
	{
		if(isset($this-> record_index[$id])) {
			// detaches first (or removes completely if it was the last reference) any existing record in that location
			unset($this[$id]);
		}

		$record->_switch_Mode(true);
		$this-> records-> attach($record);
		$this-> record_index[$id] = &$record;
	}

	/**
	 * Detaches an existing record from this RecordSet, switching it back to the Solo Mode
	 * @param int $id
	 */
	public function offsetUnset($id)
	{
		if(isset($this-> record_index[$id])) {
			// search in the main storage $this-> records for the record with this ID
			if($this-> records-> contains($this-> record_index[$id])) {
				$this-> records-> detach($this-> record_index[$id]);
				$this-> record_index[$id]->_switch_Mode(false);
			}
			// and remove from the index too
			unset($this-> record_index[$id]);
		}
	}


	/**
	 * For use with count($this) - Counts all connected records, either existing or new
	 */
	public function count()
	{
		return $this-> records-> count();
	}

	/**
	 * Use the SplObjectStorage as the iterator
	 * @see IteratorAggregate::getIterator()
	 */
	public function getIterator()
	{
		return $this-> records;
	}

}